// Created: Dec 13, 2014
//
// Author: wjinwen.1988@gmail.com

#include "chi/net/connector.h"

#include <unistd.h>
#include <functional>

#include <glog/logging.h>

#include "chi/net/event_loop.h"
#include "chi/net/inet_address.h"
#include "chi/net/socket.h"

namespace chi {
namespace net {

const int Connector::kInitRetryDelayMs = 500;
const int Connector::kMaxRetryDelayMs = 30 * 1000;

Connector::Connector(EventLoop* loop, const InetAddress& server_addr) 
  : loop_(loop),
    server_addr_(server_addr),
    connect_(false),
    state_(kConnected),
    retry_delay_ms_(kInitRetryDelayMs) {
  VLOG(11) << "Connector(this=" << this << "): ctor";
}

Connector::~Connector() {
  VLOG(11) << "Connector(this=" << this << "): dtor";
}

void Connector::set_new_conn_cb(const NewConnectionCallback& cb) {
  new_conn_cb_ = cb;
}

void Connector::Start() {
  connect_ = true;
  loop_->RunInLoop(std::bind(&Connector::StartInLoop, this));
}

void Connector::Restart() {
  set_state(States::kDisconnected);
  connect_ = true;
  StartInLoop();
}

void Connector::Stop() {
  connect_ = false;
  loop_->QueueInLoop(std::bind(&Connector::StopInLoop, this));
}

void Connector::StartInLoop() {
  if (connect_) {
    Connect();
  } 
}

void Connector::StopInLoop() {
  if (state_ == States::kConnecting) {
    set_state(States::kDisconnected);
    int sockfd = RemoveAndResetChannel();
    Retry(sockfd);
  }
}

void Connector::Connect() {
  int sockfd = Socket::CreateSocketNonblockingOrDie();
  
  const struct sockaddr_in addr = server_addr_.addr();
  int ret = ::connect(sockfd, sockaddr_cast(&addr), sizeof(addr));
  int save_errno = (ret == 0) ? 0 : errno;
  switch (save_errno) {
    case 0:
    case EINPROGRESS:
    case EINTR:
    case EISCONN:
      Connecting(sockfd);
      break;

    case EAGAIN:
    case EADDRINUSE:
    case EADDRNOTAVAIL:
    case ECONNREFUSED:
    case ENETUNREACH:
      Retry(sockfd);
      break;

    case EACCES:
    case EPERM:
    case EAFNOSUPPORT:
    case EALREADY:
    case EBADF:
    case EFAULT:
    case ENOTSOCK:
      LOG(ERROR) << "connect error in Connector::StartInLoop " << save_errno;
      break;

    default:
      LOG(ERROR) << "unexpected error in Connector::StartInLoop " << save_errno;
      ::close(sockfd);
      break;
  }
}

void Connector::Retry(int sockfd) {
  ::close(sockfd);
  set_state(States::kDisconnected);
  if (connect_) {
    LOG(INFO) << "retry connecting to " << server_addr_.ToIpPort() 
              << "in " << retry_delay_ms_ << " milliseconds";
    loop_->RunAfter(retry_delay_ms_/1000.0,
                    std::bind(&Connector::StartInLoop, this));
    retry_delay_ms_ = std::min(retry_delay_ms_*2, kMaxRetryDelayMs);
  } else {
    VLOG(11) << "do not connect";
  }
}

void Connector::Connecting(int sockfd) {
  set_state(kConnecting);
  assert(!channel_);
  channel_.reset(new Channel(loop_, sockfd));
  channel_->SetWriteCallback(
      std::bind(&Connector::HandleWrite, this));
  channel_->SetErrorCallback(
      std::bind(&Connector::HandleError, this));
  channel_->EnableWriting();
}

int Connector::RemoveAndResetChannel() {
  channel_->DisableAll();
  int sockfd = channel_->fd();
  loop_->QueueInLoop(
      std::bind(&Connector::ResetChannel, this));
  return sockfd; 
}

void Connector::ResetChannel() {
  channel_.reset();
}

void Connector::HandleWrite() {
  VLOG(11) << "Connector::HandleWrite(this=" << this << " state="
           << state_ << " host=" << server_addr_.ToIpPort() << ")";

  if (state_ == States::kConnecting) {
    int sockfd = RemoveAndResetChannel();
    int error;
    socklen_t len = sizeof(error);
    int rv = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
    if (rv != 0) {
      LOG(ERROR) << "Connector::HandleWrite(this=" << this << " fd="
                 << sockfd << " host=" << server_addr_.ToIpPort()
                 << "): error calling getsockopt after connect";
      Retry(sockfd);
      return ;
    }

    if (error != 0) {
      LOG(ERROR) << "Connector::HandleWrite(this=" << this << " fd="
                 << sockfd << " host=" << server_addr_.ToIpPort() 
                 << "): connect failed";
      Retry(sockfd);
      return ;
    }

    set_state(kConnected);
    if (connect_) {
      new_conn_cb_(sockfd);
    } else {
      ::close(sockfd);
    }
  }
}

void Connector::HandleError() {
  LOG(ERROR) << "Connector::HandleWrite(this=" << this << " state="
             << state_ << " host=" << server_addr_.ToIpPort() 
             << "): connect error";
  if (state_ == States::kConnecting) {
    int sockfd = RemoveAndResetChannel();
    int error;
    socklen_t len = sizeof(error);
    ::getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
    VLOG(11) << "SO_ERROR = " << error;
    Retry(sockfd);
  }
}

} 
}
